/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkFile, AstTreeUtils, ts } from 'arkanalyzer';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { Rule, Defects, MatcherTypes, MatcherCallback, FileMatcher } from '../../Index';
import { RuleListUtil } from "../../utils/common/DefectsList";
import { IssueReport } from '../../model/Defects';

export type Option = [{
    reportUnusedFallthroughComment?: boolean;
    allowEmptyCase?: boolean;
    commentPattern?: string;
}];
interface Violation {
    line: number;
    character: number;
    endCharacter: number;
    message: string;
    filePath?: string;
};

export class NoFallthroughCheck implements BaseChecker {
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    public rule: Rule;
    public metaData: BaseMetaData = {
        severity: 2,
        ruleDocPath: 'docs/no-fallthrough-check.md',
        description: 'Disallow unintended fallthrough cases in switch statements.',
    };

    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
    };

    public registerMatchers(): MatcherCallback[] {
        const fileMatcher: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check,
        };
        return [fileMatcher];
    };
    private defaultOptions: Option = [{}];
    public check = (target: ArkFile) => {
        this.defaultOptions = this.rule && this.rule.option[0] ? this.rule.option as Option : this.defaultOptions;
        if (target instanceof ArkFile) {
            let code = target.getCode();
            const myInvalidPositions = this.checkAction(code, target);
            myInvalidPositions.forEach((violation) => {
                violation.filePath = target.getFilePath();
                this.addIssueReport(violation);
            });
        }
    };

    private checkAction(sourceCode: string, target: ArkFile): Violation[] {
        const sourceFile = AstTreeUtils.getASTNode(target.getName(), sourceCode);
        const violations: Violation[] = [];
        const checkNode = (node: ts.Node) => {
            if (ts.isSwitchStatement(node)) {
                const cases = node.caseBlock.clauses;
                for (let i = 0; i < cases.length; i++) {
                    const currentCase = cases[i];
                    const nextCase = cases[i + 1];
                    if (nextCase) {
                        const { line, character } = sourceFile.getLineAndCharacterOfPosition(currentCase.getStart());
                        let hasFallsThroughComment = this.checkFallthroughComment(currentCase, sourceFile);
                        if (!hasFallsThroughComment) {
                            hasFallsThroughComment = this.checkFallthroughCommentBetweenCases(currentCase, nextCase, sourceCode);
                        }
                        const hasTerminator = this.hasTerminatorStatement(currentCase);
                        const isEmptyCase = this.isEmptyCase(currentCase);
                        const isMergedCase = this.isMergedCase(currentCase, nextCase);
                        this.validateCase(currentCase, hasFallsThroughComment, hasTerminator, isEmptyCase, isMergedCase, line, character, sourceFile, nextCase, violations);
                    }
                }
            }
            ts.forEachChild(node, checkNode);
        };

        checkNode(sourceFile);
        return violations;
    };

    private checkAllPathsTerminate = (node: ts.Node): boolean => {
        if (ts.isIfStatement(node)) {
            const ifBranchTerminates = this.checkAllPathsTerminate(node.thenStatement);
            const elseBranchTerminates = node.elseStatement ? this.checkAllPathsTerminate(node.elseStatement) : false;
            if (!node.elseStatement) {
                return ifBranchTerminates && this.checkAllPathsTerminate(node.parent);
            }
            return ifBranchTerminates && elseBranchTerminates;
        } else if (ts.isTryStatement(node)) {
            if (node.tryBlock) {
                const tryBlockHasBreak = node.tryBlock.statements.some(ts.isBreakStatement);
                const tryBlockHasThrow = node.tryBlock.statements.some(ts.isThrowStatement);
                if (tryBlockHasBreak) {
                    return true;
                } else if (tryBlockHasThrow) {
                    if (node.catchClause) {
                        const catchBlockHasBreak = node.catchClause.block.statements.some(ts.isBreakStatement);
                        const catchBlockHasThrow = node.catchClause.block.statements.some(ts.isThrowStatement);
                        return catchBlockHasBreak || catchBlockHasThrow;
                    } else {
                        return false;
                    }
                } else {
                    if (node.finallyBlock) {
                        const finallyBlockHasBreak = node.finallyBlock.statements.some(ts.isBreakStatement);
                        const finallyBlockHasThrow = node.finallyBlock.statements.some(ts.isThrowStatement);
                        return finallyBlockHasBreak || finallyBlockHasThrow;
                    } else {
                        return false;
                    }
                }
            } else {
                return true;
            }
        } else if (ts.isDoStatement(node)) {
            if (!node.statement || !node.expression) {
                return false;
            }
            const doBlockHasBreakOrThrow = ts.isBlock(node.statement) && node.statement.statements.some(stmt =>
                ts.isBreakStatement(stmt) || ts.isThrowStatement(stmt)
            );
            if (doBlockHasBreakOrThrow) {
                return true;
            } else {
                const whileBlockHasThrow = ts.isBlock(node.statement) && node.statement.statements.some(ts.isThrowStatement);
                return whileBlockHasThrow;
            }
        } else if (ts.isBlock(node)) {
            return node.statements.every(stmt => this.checkAllPathsTerminate(stmt));
        } else {
            return ts.isBreakStatement(node) ||
                ts.isReturnStatement(node) ||
                ts.isThrowStatement(node) ||
                ts.isContinueStatement(node);
        }
    };

    private checkFallthroughComment(node: ts.CaseOrDefaultClause, sourceFile: ts.SourceFile): boolean {
        const commentPattern = this.defaultOptions[0].commentPattern || "falls?\\s?through";
        const regex = new RegExp(commentPattern, "i");
        // 检查块语句内部的注释
        const checkBlock = (block: ts.Block): boolean => {
            const lastStmt = block.statements[block.statements.length - 1];
            if (!lastStmt) {
                if (regex.test(block.getText(sourceFile))) {
                    return true; // 找到匹配的注释
                }
                return false; // 空块返回 false
            }
            // 检查最后一个语句是否是注释
            if (lastStmt.kind === ts.SyntaxKind.SingleLineCommentTrivia || lastStmt.kind === ts.SyntaxKind.MultiLineCommentTrivia) {
                const text = lastStmt.getText(sourceFile);
                return regex.test(text); // 检查注释是否匹配
            } else {
                const text = lastStmt.getText(sourceFile);
                if (regex.test(text) || text.includes("return") || text.includes("break")) {
                    return true; // 找到匹配的注释
                }
            }
            // 检查最后一个语句的尾随注释
            const trailingComments = ts.getTrailingCommentRanges(sourceFile.text, lastStmt.end);
            if (trailingComments) {
                for (const comment of trailingComments) {
                    const text = sourceFile.text.substring(comment.pos, comment.end);
                    if (regex.test(text)) {
                        return true;
                    }
                }
            }
            return false;
        };
        const statements = node.statements;
        if (statements.length === 0) {
            return false; // 空的 case 块返回 false
        }
        // 如果 case 块只包含一个块语句
        if (statements.length === 1 && ts.isBlock(statements[0])) {
            return checkBlock(statements[0]); // 检查块语句内部的注释
        }
        // 如果 case 块包含多个语句或嵌套块
        if (statements.some(stmt => ts.isBlock(stmt) || stmt.kind === ts.SyntaxKind.Block)) {
            return false;
        }
        // 如果 case 块只包含普通语句，检查最后一个语句的注释
        const lastStmt = statements[statements.length - 1];
        if (!lastStmt) {
            return false; // 最后一个语句不存在返回 false
        }
        // 检查最后一个语句是否是注释
        if (lastStmt.kind === ts.SyntaxKind.SingleLineCommentTrivia || lastStmt.kind === ts.SyntaxKind.MultiLineCommentTrivia) {
            const text = lastStmt.getText(sourceFile);
            return regex.test(text);
        }
        // 检查最后一个语句的尾随注释
        const trailingComments = ts.getTrailingCommentRanges(sourceFile.text, lastStmt.end);
        if (trailingComments) {
            for (const comment of trailingComments) {
                const text = sourceFile.text.substring(comment.pos, comment.end);
                if (regex.test(text)) {
                    return true; // 找到匹配的注释
                }
            }
        }
        // 检查嵌套的 if、try、do...while 等语句的终止性
        return this.checkAllPathsTerminate(lastStmt);
    };

    private checkFallthroughCommentBetweenCases(currentCase: ts.CaseOrDefaultClause, nextCase: ts.CaseOrDefaultClause, sourceCode: string): boolean {
        const commentPattern = this.defaultOptions[0]?.commentPattern || "falls?\\s?through";
        const textBetweenCases = sourceCode.slice(currentCase.end, nextCase.getStart());
        return new RegExp(commentPattern, "i").test(textBetweenCases);
    };

    private hasTerminatorStatement(node: ts.CaseOrDefaultClause): boolean {
        const isTerminator = (stmt: ts.Statement): boolean => {
            return ts.isBreakStatement(stmt) ||
                ts.isReturnStatement(stmt) ||
                ts.isThrowStatement(stmt) ||
                ts.isContinueStatement(stmt);
        };
        const checkBlock = (block: ts.Block): boolean => {
            return block.statements.some(stmt => {
                if (ts.isBlock(stmt)) {
                    return checkBlock(stmt);
                }
                return isTerminator(stmt);
            });
        };
        return node.statements.some(stmt => {
            if (ts.isBlock(stmt)) {
                return checkBlock(stmt);
            }
            return isTerminator(stmt);
        });
    };

    private isEmptyCase(node: ts.CaseOrDefaultClause): boolean {
        return node.statements.length === 0 ||
            node.statements.every(stmt => {
                return ts.isEmptyStatement(stmt);
            });
    };

    private isMergedCase(currentCase: ts.CaseOrDefaultClause, nextCase: ts.CaseOrDefaultClause): boolean {
        return this.isEmptyCase(currentCase) && !this.isEmptyCase(nextCase);
    };

    private validateCase(
        currentCase: ts.CaseOrDefaultClause,
        hasFallsThroughComment: boolean,
        hasTerminator: boolean,
        isEmptyCase: boolean,
        isMergedCase: boolean,
        line: number,
        character: number,
        sourceFile: ts.SourceFile,
        nextCase: ts.CaseOrDefaultClause,
        violations: Violation[]
    ) {
        const endCharacter = nextCase.getEnd();
        const endPosition = sourceFile.getLineAndCharacterOfPosition(endCharacter);
        if (isEmptyCase) {
            this.validateEmptyCase(currentCase, nextCase, sourceFile, endPosition, violations);
        }
        // 检查未使用的落空注释
        if (this.defaultOptions[0].reportUnusedFallthroughComment) {
            this.validateUnusedFallthroughComment(currentCase, hasFallsThroughComment, sourceFile, endPosition, nextCase, violations,);
        }

        if (!hasFallsThroughComment && !hasTerminator && !isMergedCase) {
            const { line, character } = sourceFile.getLineAndCharacterOfPosition(nextCase.getStart());
            const isNextDefault = nextCase && nextCase.kind === ts.SyntaxKind.DefaultClause;
            const nextClauseType = isNextDefault ? "default" : "case";
            violations.push({
                message: `Expected a 'break' statement before '${nextClauseType}'.`,
                line: line + 1,
                character: character + 1,
                endCharacter: endPosition.character + 1
            });
        }

        if (ts.isDefaultClause(currentCase) && !hasTerminator && !hasFallsThroughComment) {
            violations.push({
                message: `Default case should have a 'break' statement or a fallthrough comment.`,
                line: line + 1,
                character: character + 1,
                endCharacter: endPosition.character + 1
            });
        }
    };

    private validateEmptyCase(
        currentCase: ts.CaseOrDefaultClause,
        nextCase: ts.CaseOrDefaultClause,
        sourceFile: ts.SourceFile,
        endPosition: ts.LineAndCharacter,
        violations: Violation[]
    ) {
        const textBetweenCases = nextCase ? sourceFile.text.slice(currentCase.end, nextCase.getStart()) : '';
        // 检查空 case
        const hasEmptyLine = /\n\s*\n/.test(textBetweenCases);
        // 检测注释（单行或多行）
        const commentPattern = this.defaultOptions[0].commentPattern || "falls?\\s?through";
        const hasComment = /(\/\/.*|\/\*[\s\S]*?\*\/)/.test(textBetweenCases);
        const { line, character } = sourceFile.getLineAndCharacterOfPosition(nextCase.getStart());
        const regex = new RegExp(commentPattern, "i");
        let a = regex.test(textBetweenCases)
        if (!this.defaultOptions[0].allowEmptyCase && (hasEmptyLine || (hasComment && !a))) {
            const isNextDefault = nextCase && nextCase.kind === ts.SyntaxKind.DefaultClause;
            const nextClauseType = isNextDefault ? "default" : "case";
            violations.push({
                message: `Expected a 'break' statement before '${nextClauseType}'.`,
                line: line + 1,
                character: character + 1,
                endCharacter: endPosition.character + 1
            });
        }
    };

    private validateUnusedFallthroughComment(currentCase: ts.CaseOrDefaultClause,
        hasFallsThroughComment: boolean,
        sourceFile: ts.SourceFile,
        endPosition: ts.LineAndCharacter,
        nextCase: ts.CaseOrDefaultClause,
        violations: Violation[]) {
        const commentPattern = this.defaultOptions[0].commentPattern || "falls?\\s?through";
        if (hasFallsThroughComment) {
            const isBlockScoped = currentCase.statements.some(stmt => ts.isBlock(stmt));
            if (isBlockScoped) {
                const block = currentCase.statements.find(ts.isBlock)!;
                const blockText = sourceFile.text.slice(block.getStart(), block.end);
                // 检查块内是否存在注释
                const hasComment = /(?:\/\/.*|\/\*[\s\S]*?\*\/)/.test(blockText);
                // 如果存在注释，检查注释是否符合 commentPattern
                if (hasComment) {
                    const hasValidCommentInBlock = blockText.match(new RegExp(commentPattern, "i")) ?? "";
                    if (hasValidCommentInBlock) {
                        const commentStart = block.getStart() + hasValidCommentInBlock.index!;
                        const { line: commentLine, character: commentChar } = sourceFile.getLineAndCharacterOfPosition(commentStart);
                        violations.push({
                            message: `Found a comment that would permit fallthrough, but case cannot fall through.`,
                            line: commentLine + 1,
                            character: commentChar - 2,
                            endCharacter: endPosition.character + 1
                        });
                    }
                }
            }
            const isBlockScopedss = currentCase.statements.some(stmt => {
                // 检查是否是终止语句
                if (
                    ts.isReturnStatement(stmt) ||
                    ts.isBreakStatement(stmt) ||
                    ts.isThrowStatement(stmt) ||
                    ts.isContinueStatement(stmt)
                ) {
                    return true;
                }
                // 检查 if...else 块
                if (ts.isIfStatement(stmt)) {
                    // 检查 if 分支是否终止
                    const ifBranchTerminates = this.checkAllPathsTerminate(stmt.thenStatement);
                    // 检查 else 分支是否终止（如果存在）
                    const elseBranchTerminates = stmt.elseStatement ? this.checkAllPathsTerminate(stmt.elseStatement) : false;
                    // 如果 if 和 else 分支都终止，则整个 if...else 块终止
                    return ifBranchTerminates && elseBranchTerminates;
                }
                // 如果不是终止语句或 if...else 块，则返回 false
                return false;
            });
            const nextCaseStart = nextCase ? nextCase.getStart() : sourceFile.text.length;
            const textUntilNextCase = sourceFile.text.slice(currentCase.end, nextCaseStart);
            const commentMatch = textUntilNextCase.match(new RegExp(commentPattern, "i"));
            if (commentMatch && isBlockScopedss) {
                const commentStart = currentCase.end + textUntilNextCase.indexOf(commentMatch[0]);
                const { line: commentLine, character: commentChar } = sourceFile.getLineAndCharacterOfPosition(commentStart);
                violations.push({
                    message: `Found a comment that would permit fallthrough, but case cannot fall through.`,
                    line: commentLine + 1,
                    character: commentChar - 2,
                    endCharacter: endPosition.character + 1
                });
            }
        }
    };

    private addIssueReport(violation: Violation) {
        this.metaData.description = violation.message;
        const severity = this.rule.alert ?? this.metaData.severity;
        const defect = new Defects(
            violation.line,
            violation.character,
            violation.endCharacter,
            this.metaData.description,
            severity,
            this.rule.ruleId,
            violation.filePath as string,
            this.metaData.ruleDocPath,
            true,
            false,
            false,
        );
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    };
}

